ZCB bootstrapping¶

In [1]:
import numpy as np 
import pandas as pd
from scipy.optimize import minimize
import plotly.graph_objects as go
import matplotlib.pyplot as plt
In [2]:
swap_curve = pd.read_excel("market_data_2023 (1).xlsx", sheet_name=1)
swap_curve = swap_curve.iloc[2:].reset_index(drop=True)
swap_curve.columns = ['Maturity', 'Swap rates (in %)']
swap_curve = swap_curve.dropna()
maturities_ex1 = swap_curve['Maturity'].values
swap_rates = swap_curve['Swap rates (in %)'].values
In [3]:
def convert_maturity(maturity):
    """Converte una scadenza in anni."""
    if 'M' in maturity:
        return int(maturity.replace('M', '')) / 12
    elif 'Y' in maturity:
        return int(maturity.replace('Y', ''))
    else:
        raise ValueError(f"Formato di maturità non riconosciuto: {maturity}")

maturities_ex1 = [convert_maturity(m) for m in maturities_ex1]
swap_rates = swap_rates / 100
In [4]:
df = pd.DataFrame({'Maturity': maturities_ex1, 'swap_rates': swap_rates})
df.head(5)
Out[4]:
Maturity swap_rates
0 0.083333 0.02516
1 0.250000 0.02647
2 0.500000 0.0308
3 0.750000 0.0314
4 1.000000 0.03239
In [5]:
maturity = df['Maturity']
rate = df['swap_rates']
In [6]:
def NelsonSiegelSvensson(time_to_maturity, b0, b1, b2, b3, t1, t2):
    predicted_yield = (
        b0 +
        b1 * (1 - np.exp(-time_to_maturity / t1)) / (time_to_maturity / t1) +
        b2 * ((1 - np.exp(-time_to_maturity / t1)) / (time_to_maturity / t1) - np.exp(-time_to_maturity / t1)) +
        b3 * ((1 - np.exp(-time_to_maturity / t2)) / (time_to_maturity / t2) - np.exp(-time_to_maturity / t2))
    )
    return predicted_yield

# Obejective Function
def NSSMinimize(params, time_to_maturity, observed_yields):
    def NSSGoodFit(params, time_to_maturity, observed_yields):
        b0, b1, b2, b3, t1, t2 = params
        predicted_yields = NelsonSiegelSvensson(time_to_maturity, b0, b1, b2, b3, t1, t2)
        residuals = predicted_yields - observed_yields
        return np.sum(residuals ** 2)
    

    
    constraints = [
        {'type': 'ineq', 'fun': lambda x: x[0]},               # b0 > 0
        {'type': 'ineq', 'fun': lambda x: x[1] + x[2]},       # b1 + b2 > 0
        {'type': 'ineq', 'fun': lambda x: x[4]},               # t1 > 0
        {'type': 'ineq', 'fun': lambda x: x[5]}                # t2 > 0
    ]

    opt_solution = minimize(
        NSSGoodFit,
        params,
        args=(time_to_maturity, observed_yields),
        method="SLSQP",
    )

    return opt_solution.x
In [7]:
initial_params_1 = (0.03, -0.02, 0.01, 0.005, 1.0, 2.0)
initial_params_2 = (0.025, -0.015, 0.008, 0.004, 1.2, 2.5) 
initial_params_3 = (0.035, -0.03, 0.015, 0.006, 0.9, 2.1)
initial_params_4 = (0.1, 1.5, 1, 0.4, 0.9, 2.)
In [8]:
# Compute 
optimal_1 = NSSMinimize(initial_params_1, maturity, rate)
optimal_2 = NSSMinimize(initial_params_2, maturity, rate)
optimal_3 = NSSMinimize(initial_params_3, maturity, rate)
optimal_4 = NSSMinimize(initial_params_4, maturity, rate)
In [9]:
mat = np.linspace(1, 30, 30) 
yields_1 = NelsonSiegelSvensson(mat, *optimal_1)
yields_2 = NelsonSiegelSvensson(mat, *optimal_2)
yields_3 = NelsonSiegelSvensson(mat, *optimal_3)
yields_4 = NelsonSiegelSvensson(mat, *optimal_4)
In [10]:
# Create DataFrames for each set of yields
new_df = pd.DataFrame({'NSSrates1': yields_1}, index=mat)
new_df['NSSrates2'] = yields_2
new_df['NSSrates3'] = yields_3
new_df['NSSrates4'] = yields_4
In [11]:
new_df.head(5)
Out[11]:
NSSrates1 NSSrates2 NSSrates3 NSSrates4
1.0 0.031240 0.030604 0.031250 0.031440
2.0 0.029729 0.029721 0.029496 0.029051
3.0 0.027030 0.027506 0.026765 0.026349
4.0 0.024972 0.025514 0.024789 0.024672
5.0 0.023743 0.024111 0.023655 0.023800
In [12]:
fig_nss = go.Figure()

# LINE
fig_nss.add_trace(go.Scatter(
    x=new_df.index, 
    y=new_df["NSSrates1"], 
    mode='lines',
    name="Model NSS1",
    line=dict(width=1.3, dash = 'dash')
))

# LINE
fig_nss.add_trace(go.Scatter(
    x=new_df.index, 
    y=new_df["NSSrates2"], 
    mode='lines',
    name="Model NSS2",
    line=dict(width=1.3, dash = 'dash')
))

# LINE
fig_nss.add_trace(go.Scatter(
    x=new_df.index, 
    y=new_df["NSSrates3"], 
    mode='lines',
    name="Model NSS3",
    line=dict(width=1.3, dash = 'dash')
))

# LINE
fig_nss.add_trace(go.Scatter(
    x=new_df.index, 
    y=new_df["NSSrates4"], 
    mode='lines',
    name="Model NSS4",
    line=dict(width=1.3, dash = 'dash')
))


#SCATTER
fig_nss.add_trace(go.Scatter(
    x=df.Maturity, 
    y=df["swap_rates"], 
    mode='markers',
    name="Swap Rates",
    marker=dict(size=6 ,color = 'orangered')
))


fig_nss.update_layout(
    title="Model NSS vs Swap Rates",
    xaxis_title="Maturity",
    yaxis_title="Value",
    font=dict(family="Times New Roman", size=10, color="Black"),
    template="plotly_white",
    showlegend=True,
    width=1000,  
    height=500   
)

# Show the figure
fig_nss.show()
In [13]:
Maturity = np.linspace(-3,30,34)
df3 = pd.DataFrame({'Maturity':Maturity})
df3.loc[0] = [0.083333]
df3.loc[1] = [0.25]
df3.loc[2] = [0.5]
df3.loc[3] = [0.75]

swap_rates = [0.02516, 0.02647, 0.0308, 0.0314]  
df3["swap_rates"] = np.nan  
df3.loc[:3, "swap_rates"] = swap_rates  
df3.loc[4:, "swap_rates"] = new_df["NSSrates1"].values
df3
Out[13]:
Maturity swap_rates
0 0.083333 0.025160
1 0.250000 0.026470
2 0.500000 0.030800
3 0.750000 0.031400
4 1.000000 0.031240
5 2.000000 0.029729
6 3.000000 0.027030
7 4.000000 0.024972
8 5.000000 0.023743
9 6.000000 0.023137
10 7.000000 0.022928
11 8.000000 0.022947
12 9.000000 0.023087
13 10.000000 0.023282
14 11.000000 0.023496
15 12.000000 0.023708
16 13.000000 0.023909
17 14.000000 0.024096
18 15.000000 0.024265
19 16.000000 0.024419
20 17.000000 0.024557
21 18.000000 0.024682
22 19.000000 0.024796
23 20.000000 0.024898
24 21.000000 0.024991
25 22.000000 0.025076
26 23.000000 0.025154
27 24.000000 0.025226
28 25.000000 0.025292
29 26.000000 0.025352
30 27.000000 0.025409
31 28.000000 0.025461
32 29.000000 0.025510
33 30.000000 0.025555
In [14]:
df3["ZCB_price"] = np.nan
df3["Zero_Rate"] = np.nan
notional = 1.0


for i, row in df3.iterrows():
    if row["Maturity"] <= 1.0:
        rate = row["swap_rates"]
        df3.loc[i, "Zero_Rate"] = rate  
        df3.loc[i, "ZCB_price"] = np.exp(-rate * row["Maturity"]) 

        
for i, row in df3.iterrows():
    if row["Maturity"] > 1.0:
        previous_cashflows = [
            (df3.iloc[j]["swap_rates"] * notional / 2 ) * df3.iloc[j]["ZCB_price"]
            for j in range(int(i))  
        ]
        total_cashflows = sum(previous_cashflows)

        maturity = row["Maturity"]
        coupon = row["swap_rates"] * notional / 2 
        principal_discount = notional  

        zcb_price = (notional - total_cashflows) / (coupon + principal_discount)
        zero_rate = -np.log(zcb_price) / maturity
        df3.loc[i, "ZCB_price"] = zcb_price
        df3.loc[i, "Zero_Rate"] = zero_rate
In [15]:
df3
Out[15]:
Maturity swap_rates ZCB_price Zero_Rate
0 0.083333 0.025160 0.997906 0.025160
1 0.250000 0.026470 0.993404 0.026470
2 0.500000 0.030800 0.984718 0.030800
3 0.750000 0.031400 0.976725 0.031400
4 1.000000 0.031240 0.969243 0.031240
5 2.000000 0.029729 0.915058 0.044384
6 3.000000 0.027030 0.902856 0.034064
7 4.000000 0.024972 0.891722 0.028650
8 5.000000 0.023743 0.881260 0.025281
9 6.000000 0.023137 0.871182 0.022984
10 7.000000 0.022928 0.861308 0.021329
11 8.000000 0.022947 0.851538 0.020089
12 9.000000 0.023087 0.841820 0.019132
13 10.000000 0.023282 0.832133 0.018376
14 11.000000 0.023496 0.822471 0.017767
15 12.000000 0.023708 0.812836 0.017269
16 13.000000 0.023909 0.803233 0.016855
17 14.000000 0.024096 0.793671 0.016506
18 15.000000 0.024265 0.784157 0.016210
19 16.000000 0.024419 0.774699 0.015955
20 17.000000 0.024557 0.765302 0.015734
21 18.000000 0.024682 0.755972 0.015542
22 19.000000 0.024796 0.746715 0.015372
23 20.000000 0.024898 0.737533 0.015222
24 21.000000 0.024991 0.728431 0.015089
25 22.000000 0.025076 0.719411 0.014969
26 23.000000 0.025154 0.710475 0.014862
27 24.000000 0.025226 0.701626 0.014765
28 25.000000 0.025292 0.692864 0.014677
29 26.000000 0.025352 0.684191 0.014597
30 27.000000 0.025409 0.675608 0.014524
31 28.000000 0.025461 0.667115 0.014457
32 29.000000 0.025510 0.658713 0.014395
33 30.000000 0.025555 0.650403 0.014339
In [16]:
save = df3.to_csv('ZCB_prices.csv')